/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017, 2018, 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import React, { ReactNode } from "react";
import { logger } from "matrix-js-sdk/src/logger";
import { sleep } from "matrix-js-sdk/src/utils";

import { _t, _td } from "../../../languageHandler";
import Modal from "../../../Modal";
import PasswordReset from "../../../PasswordReset";
import AuthPage from "../../views/auth/AuthPage";
import PassphraseField from "../../views/auth/PassphraseField";
import { PASSWORD_MIN_SCORE } from "../../views/auth/RegistrationForm";
import AuthHeader from "../../views/auth/AuthHeader";
import AuthBody from "../../views/auth/AuthBody";
import PassphraseConfirmField from "../../views/auth/PassphraseConfirmField";
import StyledCheckbox from "../../views/elements/StyledCheckbox";
import { ValidatedServerConfig } from "../../../utils/ValidatedServerConfig";
import { Icon as CheckboxIcon } from "../../../../res/img/compound/checkbox-32px.svg";
import { Icon as LockIcon } from "../../../../res/img/compound/padlock-32px.svg";
import QuestionDialog from "../../views/dialogs/QuestionDialog";
import { EnterEmail } from "./forgot-password/EnterEmail";
import { CheckEmail } from "./forgot-password/CheckEmail";
import Field from "../../views/elements/Field";
import { ErrorMessage } from "../ErrorMessage";
import { VerifyEmailModal } from "./forgot-password/VerifyEmailModal";
import Spinner from "../../views/elements/Spinner";
import { formatSeconds } from "../../../DateUtils";
import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils";

const emailCheckInterval = 2000;

enum Phase {
    // Show email input
    EnterEmail = 1,
    // Email is in the process of being sent
    SendingEmail = 2,
    // Email has been sent
    EmailSent = 3,
    // Show new password input
    PasswordInput = 4,
    // Password is in the process of being reset
    ResettingPassword = 5,
    // All done
    Done = 6,
}

interface Props {
    serverConfig: ValidatedServerConfig;
    onLoginClick: () => void;
    onComplete: () => void;
}

interface State {
    phase: Phase;
    email: string;
    password: string;
    password2: string;
    errorText: string | ReactNode | null;

    // We perform liveliness checks later, but for now suppress the errors.
    // We also track the server dead errors independently of the regular errors so
    // that we can render it differently, and override any other error the user may
    // be seeing.
    serverIsAlive: boolean;
    serverDeadError: string;

    logoutDevices: boolean;
}

export default class ForgotPassword extends React.Component<Props, State> {
    private reset: PasswordReset;
    private fieldPassword: Field | null = null;
    private fieldPasswordConfirm: Field | null = null;

    public constructor(props: Props) {
        super(props);
        this.state = {
            phase: Phase.EnterEmail,
            email: "",
            password: "",
            password2: "",
            errorText: null,
            // We perform liveliness checks later, but for now suppress the errors.
            // We also track the server dead errors independently of the regular errors so
            // that we can render it differently, and override any other error the user may
            // be seeing.
            serverIsAlive: true,
            serverDeadError: "",
            logoutDevices: false,
        };
        this.reset = new PasswordReset(this.props.serverConfig.hsUrl, this.props.serverConfig.isUrl);
    }

    public componentDidUpdate(prevProps: Readonly<Props>): void {
        if (
            prevProps.serverConfig.hsUrl !== this.props.serverConfig.hsUrl ||
            prevProps.serverConfig.isUrl !== this.props.serverConfig.isUrl
        ) {
            // Do a liveliness check on the new URLs
            this.checkServerLiveliness(this.props.serverConfig);
        }
    }

    private async checkServerLiveliness(serverConfig: ValidatedServerConfig): Promise<void> {
        try {
            await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(serverConfig.hsUrl, serverConfig.isUrl);

            this.setState({
                serverIsAlive: true,
            });
        } catch (e: any) {
            const { serverIsAlive, serverDeadError } = AutoDiscoveryUtils.authComponentStateForError(
                e,
                "forgot_password",
            );
            this.setState({
                serverIsAlive,
                errorText: serverDeadError,
            });
        }
    }

    private async onPhaseEmailInputSubmit(): Promise<void> {
        this.phase = Phase.SendingEmail;

        if (await this.sendVerificationMail()) {
            this.phase = Phase.EmailSent;
            return;
        }

        this.phase = Phase.EnterEmail;
    }

    private sendVerificationMail = async (): Promise<boolean> => {
        try {
            await this.reset.requestResetToken(this.state.email);
            return true;
        } catch (err: any) {
            this.handleError(err);
        }

        return false;
    };

    private handleError(err: any): void {
        if (err?.httpStatus === 429) {
            // 429: rate limit
            const retryAfterMs = parseInt(err?.data?.retry_after_ms, 10);

            const errorText = isNaN(retryAfterMs)
                ? _t("auth|reset_password|rate_limit_error")
                : _t("auth|reset_password|rate_limit_error_with_time", {
                      timeout: formatSeconds(retryAfterMs / 1000),
                  });

            this.setState({
                errorText,
            });
            return;
        }

        if (err?.name === "ConnectionError") {
            this.setState({
                errorText: _t("cannot_reach_homeserver") + ": " + _t("cannot_reach_homeserver_detail"),
            });
            return;
        }

        this.setState({
            errorText: err.message,
        });
    }

    private async onPhaseEmailSentSubmit(): Promise<void> {
        this.setState({
            phase: Phase.PasswordInput,
        });
    }

    private set phase(phase: Phase) {
        this.setState({ phase });
    }

    private async verifyFieldsBeforeSubmit(): Promise<boolean> {
        const fieldIdsInDisplayOrder = [this.fieldPassword, this.fieldPasswordConfirm];

        const invalidFields: Field[] = [];

        for (const field of fieldIdsInDisplayOrder) {
            if (!field) continue;

            const valid = await field.validate({ allowEmpty: false });
            if (!valid) {
                invalidFields.push(field);
            }
        }

        if (invalidFields.length === 0) {
            return true;
        }

        // Focus on the first invalid field, then re-validate,
        // which will result in the error tooltip being displayed for that field.
        invalidFields[0].focus();
        invalidFields[0].validate({ allowEmpty: false, focused: true });

        return false;
    }

    private async onPhasePasswordInputSubmit(): Promise<void> {
        if (!(await this.verifyFieldsBeforeSubmit())) return;

        if (this.state.logoutDevices) {
            const logoutDevicesConfirmation = await this.renderConfirmLogoutDevicesDialog();
            if (!logoutDevicesConfirmation) return;
        }

        this.phase = Phase.ResettingPassword;
        this.reset.setLogoutDevices(this.state.logoutDevices);

        try {
            await this.reset.setNewPassword(this.state.password);
            this.setState({ phase: Phase.Done });
            return;
        } catch (err: any) {
            if (err.httpStatus !== 401) {
                // 401 = waiting for email verification, else unknown error
                this.handleError(err);
                return;
            }
        }

        const modal = Modal.createDialog(
            VerifyEmailModal,
            {
                email: this.state.email,
                errorText: this.state.errorText,
                onCloseClick: () => {
                    modal.close();
                    this.setState({ phase: Phase.PasswordInput });
                },
                onReEnterEmailClick: () => {
                    modal.close();
                    this.setState({ phase: Phase.EnterEmail });
                },
                onResendClick: this.sendVerificationMail,
            },
            "mx_VerifyEMailDialog",
            false,
            false,
            {
                onBeforeClose: async (reason?: string): Promise<boolean> => {
                    if (reason === "backgroundClick") {
                        // Modal dismissed by clicking the background.
                        // Go one phase back.
                        this.setState({ phase: Phase.PasswordInput });
                    }

                    return true;
                },
            },
        );

        // Don't retry if the phase changed. For example when going back to email input.
        while (this.state.phase === Phase.ResettingPassword) {
            try {
                await this.reset.setNewPassword(this.state.password);
                this.setState({ phase: Phase.Done });
                modal.close();
            } catch (e) {
                // Email not confirmed, yet. Retry after a while.
                await sleep(emailCheckInterval);
            }
        }
    }

    private onSubmitForm = async (ev: React.FormEvent): Promise<void> => {
        ev.preventDefault();

        // Should not happen because of disabled forms, but just return if currently doing an action.
        if ([Phase.SendingEmail, Phase.ResettingPassword].includes(this.state.phase)) return;

        this.setState({
            errorText: "",
        });

        // Refresh the server errors. Just in case the server came back online of went offline.
        await this.checkServerLiveliness(this.props.serverConfig);

        // Server error
        if (!this.state.serverIsAlive) return;

        switch (this.state.phase) {
            case Phase.EnterEmail:
                this.onPhaseEmailInputSubmit();
                break;
            case Phase.EmailSent:
                this.onPhaseEmailSentSubmit();
                break;
            case Phase.PasswordInput:
                this.onPhasePasswordInputSubmit();
                break;
        }
    };

    private onInputChanged = (
        stateKey: "email" | "password" | "password2",
        ev: React.FormEvent<HTMLInputElement>,
    ): void => {
        let value = ev.currentTarget.value;
        if (stateKey === "email") value = value.trim();
        this.setState({
            [stateKey]: value,
        } as Pick<State, typeof stateKey>);
    };

    public renderEnterEmail(): JSX.Element {
        return (
            <EnterEmail
                email={this.state.email}
                errorText={this.state.errorText}
                homeserver={this.props.serverConfig.hsName}
                loading={this.state.phase === Phase.SendingEmail}
                onInputChanged={this.onInputChanged}
                onLoginClick={this.props.onLoginClick!} // set by default props
                onSubmitForm={this.onSubmitForm}
            />
        );
    }

    public async renderConfirmLogoutDevicesDialog(): Promise<boolean> {
        const { finished } = Modal.createDialog(QuestionDialog, {
            title: _t("common|warning"),
            description: (
                <div>
                    <p>{_t("auth|reset_password|other_devices_logout_warning_1")}</p>
                    <p>{_t("auth|reset_password|other_devices_logout_warning_2")}</p>
                </div>
            ),
            button: _t("action|continue"),
        });
        const [confirmed] = await finished;
        return !!confirmed;
    }

    public renderCheckEmail(): JSX.Element {
        return (
            <CheckEmail
                email={this.state.email}
                errorText={this.state.errorText}
                onReEnterEmailClick={() => this.setState({ phase: Phase.EnterEmail })}
                onResendClick={this.sendVerificationMail}
                onSubmitForm={this.onSubmitForm}
            />
        );
    }

    public renderSetPassword(): JSX.Element {
        const submitButtonChild =
            this.state.phase === Phase.ResettingPassword ? <Spinner w={16} h={16} /> : _t("auth|reset_password_action");

        return (
            <>
                <LockIcon className="mx_AuthBody_lockIcon" />
                <h1>{_t("auth|reset_password_title")}</h1>
                <form onSubmit={this.onSubmitForm}>
                    <fieldset disabled={this.state.phase === Phase.ResettingPassword}>
                        <div className="mx_AuthBody_fieldRow">
                            <PassphraseField
                                name="reset_password"
                                type="password"
                                label={_td("auth|change_password_new_label")}
                                value={this.state.password}
                                minScore={PASSWORD_MIN_SCORE}
                                fieldRef={(field) => (this.fieldPassword = field)}
                                onChange={this.onInputChanged.bind(this, "password")}
                                autoComplete="new-password"
                            />
                            <PassphraseConfirmField
                                name="reset_password_confirm"
                                label={_td("auth|reset_password|confirm_new_password")}
                                labelRequired={_td("auth|reset_password|password_not_entered")}
                                labelInvalid={_td("auth|reset_password|passwords_mismatch")}
                                value={this.state.password2}
                                password={this.state.password}
                                fieldRef={(field) => (this.fieldPasswordConfirm = field)}
                                onChange={this.onInputChanged.bind(this, "password2")}
                                autoComplete="new-password"
                            />
                        </div>
                        <div className="mx_AuthBody_fieldRow">
                            <StyledCheckbox
                                onChange={() => this.setState({ logoutDevices: !this.state.logoutDevices })}
                                checked={this.state.logoutDevices}
                            >
                                {_t("auth|reset_password|sign_out_other_devices")}
                            </StyledCheckbox>
                        </div>
                        {this.state.errorText && <ErrorMessage message={this.state.errorText} />}
                        <button type="submit" className="mx_Login_submit">
                            {submitButtonChild}
                        </button>
                    </fieldset>
                </form>
            </>
        );
    }

    public renderDone(): JSX.Element {
        return (
            <>
                <CheckboxIcon className="mx_Icon mx_Icon_32 mx_Icon_accent" />
                <h1>{_t("auth|reset_password|reset_successful")}</h1>
                {this.state.logoutDevices ? <p>{_t("auth|reset_password|devices_logout_success")}</p> : null}
                <input
                    className="mx_Login_submit"
                    type="button"
                    onClick={this.props.onComplete}
                    value={_t("auth|reset_password|return_to_login")}
                />
            </>
        );
    }

    public render(): React.ReactNode {
        let resetPasswordJsx: JSX.Element;

        switch (this.state.phase) {
            case Phase.EnterEmail:
            case Phase.SendingEmail:
                resetPasswordJsx = this.renderEnterEmail();
                break;
            case Phase.EmailSent:
                resetPasswordJsx = this.renderCheckEmail();
                break;
            case Phase.PasswordInput:
            case Phase.ResettingPassword:
                resetPasswordJsx = this.renderSetPassword();
                break;
            case Phase.Done:
                resetPasswordJsx = this.renderDone();
                break;
            default:
                // This should not happen. However, it is logged and the user is sent to the start.
                logger.warn(`unknown forgot password phase ${this.state.phase}`);
                this.setState({
                    phase: Phase.EnterEmail,
                });
                return;
        }

        return (
            <AuthPage>
                <AuthHeader />
                <AuthBody className="mx_AuthBody_forgot-password">{resetPasswordJsx}</AuthBody>
            </AuthPage>
        );
    }
}
